﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;
using System.Xml.Linq;
using LockCrypt.Core;
using Lime49;

namespace LockCrypt.DAL {
    /// <summary>
    /// Parses a LockCrypt 2.x XML file.
    /// </summary>
    public class LegacyXMLParser : IReadOnlyDatabase {
        private readonly string _fileName;
        private readonly ILogProvider _logger;

        /// <summary>
        /// Initializes a new <see cref="LegacyXMLParser"/>.
        /// </summary>
        /// <param name="fileName">Name of the file to parse.</param>
        /// <param name="logger">The logger (can be <c>null</c>).</param>
        public LegacyXMLParser(string fileName, ILogProvider logger) {
            _fileName = fileName;
            _logger = logger == null ? new ConsoleLogger() : logger;
        }

        /// <summary>
        /// Attempts to load the XML document without parsing it's contents.
        /// </summary>
        /// <returns><c>true</c> if the file can be opened, otherwise false.</returns>
        public bool CanLoadContents() {
            try {
                string rawXml = File.ReadAllText(_fileName, Encoding.UTF8);
                XDocument doc = XDocument.Parse(rawXml, LoadOptions.None);
                return doc.Element("pwlist") != null; // if it has a valid root, it may be valid.
            } catch {
                return false;
            }
        }

        /// <summary>
        /// Reads the whole contents of the file.
        /// </summary>
        /// <returns>The whole contents of the file.</returns>
        public DatabaseContents ReadDatabase() {
            if(!File.Exists(_fileName))
                throw new LockCryptException("File does not exist", 232);
            string rawXml = File.ReadAllText(_fileName, Encoding.UTF8);
            XDocument doc = XDocument.Parse(rawXml, LoadOptions.None);
            DatabaseContents contents = new DatabaseContents();
            XElement xRoot = doc.Element("pwlist");
            if(xRoot == null)
                throw new LockCryptException("Invalid Legacy Database", 233);

            IEnumerable<XElement> accountElements = xRoot.Elements("pwentry");
            Dictionary<string, string> groupIds = new Dictionary<string, string>(); // <Name, ID>
            foreach(var accountElement in accountElements) {
                try {
                    IAccount acc = new AccountBase() {
                        Name = HtmlUtils.HtmlDecode(accountElement.Element("title").Value),
                        Icon = accountElement.Element("icon").Value,
                        Created = DateTime.Parse(accountElement.Element("creationtime").Value),
                        Modified = DateTime.Parse(accountElement.Element("lastmodtime").Value),
                        Notes = HtmlUtils.HtmlDecode(accountElement.Element("notes").Value),
                        Template = new TemplateBase() {
                            Name = HtmlUtils.HtmlDecode(accountElement.Element("type").Value),
                            ID = HtmlUtils.HtmlDecode(accountElement.Element("typeid").Value),
                            ShowNotes = Convert.ToBoolean(accountElement.Element("shownotes").Value),
                        },
                    };
                    string groupName = HtmlUtils.HtmlDecode(accountElement.Element("group").Value);
                    if(groupIds.ContainsKey(groupName)) {
                        acc.ParentID = groupIds[groupName];
                    } else {
                        string newParentId = Utils.GenID(true);
                        acc.ParentID = newParentId;
                        groupIds.Add(groupName, newParentId);
                    }
                    IEnumerable<XElement> fieldElements = accountElement.Element("fields").Elements("field");
                    int sortIndex = 0;
                    foreach(var fieldElement in fieldElements) {
                        try {
                            IField field = ParseField(fieldElement);
                            if(field.Name.Equals("autotype", StringComparison.InvariantCultureIgnoreCase)) {
                                acc.AutoTypeConfig = new LockCrypt.Core.AutoType.AutoTypeConfiguration(field.Value);
                            } else {
                                field.SortIndex = sortIndex++;
                                acc.Fields.Add(field);
                            }
                        } catch(Exception ex) {
                            _logger.Log(string.Format("Error parsing field for account '{0}: {1}", acc.Name, ex.Message), true);
                        }
                    }
                    _logger.Log(string.Format("Parsed account '{0}", acc.Name), StatusMessage.StatusMessageType.Success);
                    contents.Accounts.Add(acc);
                } catch(Exception ex) {
                    _logger.Log("Error parsing account: " + ex.Message, true);
                }
            }

            contents.Groups = groupIds.Select(g => new GroupBase() {
                ID = g.Value,
                Name = HtmlUtils.HtmlDecode(g.Key)
            }).Cast<IGroup>().ToList();

            IEnumerable<XElement> templateElements = xRoot.Elements("type");
            foreach(var templateElement in templateElements) {
                try {
                    ITemplate template = new TemplateBase() {
                        Name = HtmlUtils.HtmlDecode(templateElement.Element("title").Value),
                        ID = HtmlUtils.HtmlDecode(templateElement.Element("id").Value),
                        ShowNotes = Convert.ToBoolean(templateElement.Element("shownotes").Value)
                    };
                    IEnumerable<XElement> fieldElements = templateElement.Elements("field");
                    int sortIndex = 0;
                    foreach(var fieldElement in fieldElements) {
                        try {
                            IField field = ParseField(fieldElement);
                            field.SortIndex = sortIndex++;
                            template.Fields.Add(field);
                        } catch(Exception ex) {
                            _logger.Log(string.Format("Error parsing field for account '{0}: {1}", template.Name, ex.Message), true);
                        }
                    }
                    _logger.Log(string.Format("Parsed template '{0}", template.Name), StatusMessage.StatusMessageType.Success);
                    contents.Templates.Add(template);
                } catch(Exception ex) {
                    _logger.Log("Error parsing template: " + ex.Message, true);
                }
            }
            return contents;
        }

        /// <summary>
        /// Parses the field name and it's meta data.
        /// </summary>
        /// <param name="fieldElement">The field element.</param>
        /// <returns>An IField containing the field's name and meta data.</returns>
        private static IField ParseField(XElement fieldElement) {
            IField field = new FieldBase() { Value = HtmlUtils.HtmlDecode(fieldElement.Value) };
            XAttribute attrName = fieldElement.Attribute("name");
            string fieldName = HtmlUtils.HtmlDecode(attrName == null ? fieldElement.Value : attrName.Value);
            string rawName; // the html encoded name
            if(fieldName.Length > 3 && fieldName.StartsWith("~}")) {
                switch(fieldName[2]) {
                    case '*':
                        field.Meta = FieldMetaData.Password;
                        rawName = fieldName.Substring(3);
                        break;
                    case 'u':
                        field.Meta = FieldMetaData.Url;
                        rawName = fieldName.Substring(3);
                        break;
                    case '#':
                        field.Meta = FieldMetaData.Phone;
                        rawName = fieldName.Substring(3);
                        break;
                    case '!': // usually autotype
                        field.Meta = FieldMetaData.None;
                        rawName = fieldName.Substring(3);
                        break;
                    default:
                        field.Meta = FieldMetaData.None;
                        rawName = fieldName;
                        break;
                }
            } else {
                rawName = fieldName;
            }
            field.Name = HtmlUtils.HtmlDecode(rawName);
            return field;
        }
    }
}
